View Javadoc

1   /*
2    * Copyright (C) 1998-2000 Semiotek Inc.  All Rights Reserved.
3    *
4    * Redistribution and use in source and binary forms, with or without
5    * modification, are permitted under the terms of either of the following
6    * Open Source licenses:
7    *
8    * The GNU General Public License, version 2, or any later version, as
9    * published by the Free Software Foundation
10   * (http://www.fsf.org/copyleft/gpl.html);
11   *
12   *  or
13   *
14   * The Semiotek Public License (http://webmacro.org/LICENSE.)
15   *
16   * This software is provided "as is", with NO WARRANTY, not even the
17   * implied warranties of fitness to purpose, or merchantability. You
18   * assume all risks and liabilities associated with its use.
19   *
20   * See www.webmacro.org for more information on the WebMacro project.
21   */
22  
23  
24  package org.webmacro.servlet;
25  
26  import org.webmacro.*;
27  import org.webmacro.util.LogSystem;
28  
29  import javax.servlet.ServletConfig;
30  import javax.servlet.ServletException;
31  import javax.servlet.http.HttpServlet;
32  import javax.servlet.http.HttpServletRequest;
33  import javax.servlet.http.HttpServletResponse;
34  import java.io.*;
35  import java.lang.reflect.Method;
36  import java.util.Locale;
37  
38  /***
39   * This is an abstract base class which can be used
40   * to implement a kind of WebMacro servlet. You
41   * can either subclass from it directly, or make use of one of the
42   * generic subclasses provided.
43   * <p>
44   * It's primary function is to create a WebContext and manage a
45   * Broker. It also provides a couple of convenience functions
46   * that access the Broker and/or WebContext to make some commonly
47   * accessed services more readily available.
48   * <p>
49   * @see org.webmacro.Broker
50   */
51  abstract public class WMServlet extends HttpServlet implements WebMacro
52  {
53  
54      private WebMacro _wm = null;
55      private Broker _broker = null;
56      private boolean _started = false;
57      /***
58       * The name of the config entry we look for to find out what to
59       * call the variable used in the ERROR_TEMPLATE
60       */
61      final static String ERROR_VARIABLE = "ErrorVariable";
62  
63      /***
64       * The name of the error template we will use if something
65       * goes wrong
66       */
67      final static String ERROR_TEMPLATE = "ErrorTemplate";
68  
69      /***
70       * Defaults for error variable and error template
71       */
72      final static String ERROR_TEMPLATE_DEFAULT = "error.wm";
73      final static String ERROR_VARIABLE_DEFAULT = "error";
74  
75      /***
76       * Log object used to write out messages
77       */
78      protected Log _log;
79  
80      /***
81       * null means all OK
82       */
83      private String _problem = "Not yet initialized: Your servlet API tried to access WebMacro without first calling init()!!!";
84  
85      /***
86       * This is the old-style init method, it just calls init(), after
87       * handing the ServletConfig object to the superclass
88       * @exception ServletException if it failed to initialize
89       */
90      public synchronized void init (ServletConfig sc)
91              throws ServletException
92      {
93          super.init(sc);
94          init();
95      }
96  
97      /***
98       * This method is called by the servlet runner--do not call it. It
99       * must not be overidden because it manages a shared instance
100      * of the broker--you can overide the start() method instead, which
101      * is called just after the broker is initialized.
102      */
103     public synchronized void init ()
104     {
105 
106         if (_started)
107         {
108             return;
109         }
110       
111         // locate a Broker
112       
113         if (_wm == null)
114         {
115             try
116             {
117                 _wm = initWebMacro();
118                 _broker = _wm.getBroker();
119             }
120             catch (InitException e)
121             {
122                 _problem = "Could not initialize the broker!\n\n"
123                         + "*** Check that WebMacro.properties was in your servlet\n"
124                         + "*** classpath, in a similar place to webmacro.jar \n"
125                         + "*** and that all values were set correctly.\n\n"
126                         + e.getMessage();
127                 Log sysLog = LogSystem.getSystemLog("servlet");
128                 sysLog.error(_problem, e);
129                 return;
130             }
131         }
132         _log = _broker.getLog("servlet", "WMServlet lifecycle information");
133       
134         try
135         {
136             if (_log.loggingDebug())
137             {
138                 java.net.URL url = getBroker().getResource(Broker.WEBMACRO_PROPERTIES);
139                 if (url != null)
140                     _log.debug("Using properties from " + url.toExternalForm());
141                 else
142                     _log.debug("No WebMacro.properties file was found.");
143             }
144             start();
145             _problem = null;
146         }
147         catch (ServletException e)
148         {
149             _problem = "WebMacro application code failed to initialize: \n"
150                     + e + "\n" + "This error is the result of a failure in the\n"
151                     + "code supplied by the application programmer.\n";
152             _log.error(_problem, e);
153         }
154         _log.notice("started: " + this);
155         _started = true;
156 
157     }
158 
159     /***
160      * This method is called by the servlet runner--do not call it. It
161      * must not be overidden because it manages a shared instance of
162      * the broker--you can overide the stop() method instead, which
163      * will be called just before the broker is shut down.  Once the
164      * stop() method has been called, the nested WM will be destroyed
165      * and then the parent destroy() method will be invoked.
166      * @see WM#destroy()
167      */
168     public synchronized void destroy ()
169     {
170         stop();
171         _wm.destroy();
172         _log.notice("stopped: " + this);
173         _wm = null;
174         _started = false;
175         super.destroy();
176     }
177 
178 
179     // SERVLET API METHODS
180    
181     /***
182      * Process an incoming GET request: Builds a WebContext up and then
183      * passes it to the handle() method. You can overide this if you want,
184      * though for most purposes you are expected to overide handle()
185      * instead.
186      * <p>
187      * @param req the request we got
188      * @param resp the response we are generating
189      * @exception ServletException if we can't get our configuration
190      * @exception IOException if we can't write to the output stream
191      */
192     protected void doGet (HttpServletRequest req, HttpServletResponse resp)
193             throws ServletException, IOException
194     {
195         doRequest(req, resp);
196     }
197 
198     /***
199      * Behaves exactly like doGet() except that it reads data from POST
200      * before doing exactly the same thing. This means that you can use
201      * GET and POST interchangeably with WebMacro. You can overide this if
202      * you want, though for most purposes you are expected to overide
203      * handle() instead.
204      * <p>
205      * @param req the request we got
206      * @param resp the response we are generating
207      * @exception ServletException if we can't get our configuration
208      * @exception IOException if we can't read/write to the streams we got
209      */
210     protected void doPost (HttpServletRequest req, HttpServletResponse resp)
211             throws ServletException, IOException
212     {
213         doRequest(req, resp);
214     }
215 
216     final private void doRequest (
217             HttpServletRequest req, HttpServletResponse resp)
218             throws IOException
219     {
220 
221         WebContext context = null;
222 
223         if (_problem != null)
224         {
225             init();
226             if (_problem != null)
227             {
228                 try
229                 {
230                     resp.setContentType("text/html");
231                     Writer out = resp.getWriter();
232 
233                     out.write("<html><head><title>WebMacro Error</title></head>");
234                     out.write("<body><h1><font color=\"red\">WebMacro Error: ");
235                     out.write("</font></h1><pre>");
236                     out.write(_problem);
237                     out.write("</pre>");
238                     out.write("Please contact the server administrator");
239                     out.flush();
240                     out.close();
241                 }
242                 catch (Exception e)
243                 {
244                     _log.error(_problem, e);
245                 }
246                 return;
247             }
248         }
249 
250 
251         context = newWebContext(req, resp);
252         try
253         {
254             Template t;
255             t = handle(context);
256 
257             if (t != null)
258             {
259                 execute(t, context);
260             }
261             destroyContext(context);
262         }
263         catch (HandlerException e)
264         {
265             _log.error("Your handler failed to handle the request:" + this, e);
266             Template tmpl = error(context,
267                     "Your handler was unable to process the request successfully " +
268                     "for some reason. Here are the details:<p>" +
269                     "<pre>" + e + "</pre>");
270             execute(tmpl, context);
271         }
272         catch (Exception e)
273         {
274             _log.error("Your handler failed to handle the request:" + this, e);
275             Template tmpl = error(context,
276                     "The handler WebMacro used to handle this request failed for " +
277                     "some reason. This is likely a bug in the handler written " +
278                     "for this application. Here are the details:<p>" +
279                     "<pre>" + e + "</pre>");
280             execute(tmpl, context);
281         }
282     }
283    
284    
285     // CONVENIENCE METHODS & ACCESS TO THE BROKER
286    
287     /***
288      * Create an error template using the built in error handler.
289      * This is useful for returning error messages on failure;
290      * it is used by WMServlet to display errors resulting from
291      * any exception that you may throw from the handle() method.
292      * @param context will add error variable to context (see Config)
293      * @param error a string explaining what went wrong
294      */
295     protected Template error (WebContext context, String error)
296     {
297         Template tmpl = null;
298         //Handler hand = new ErrorHandler();
299         try
300         {
301             context.put(getErrorVariableName(),
302                     error);
303             //tmpl = hand.accept(context);
304             tmpl = getErrorTemplate();
305         }
306         catch (Exception e2)
307         {
308             _log.error("Unable to use ErrorHandler", e2);
309         }
310         return tmpl;
311     }
312 
313     /***
314      * <p>Returns the name of the error variable, as per the config.</p>
315      * @return Name to use for the error variable in templates
316      */
317     protected String getErrorVariableName ()
318     {
319         return getConfig(ERROR_VARIABLE, ERROR_VARIABLE_DEFAULT);
320     }
321 
322     /***
323      * This object is used to access components that have been plugged
324      * into WebMacro; it is shared between all instances of this class and
325      * its subclasses. It is created when the first instance is initialized,
326      * and deleted when the last instance is shut down. If you attempt to
327      * access it after the last servlet has been shutdown, it will either
328      * be in a shutdown state or else null.
329      */
330     public Broker getBroker ()
331     {
332         // this method can be unsynch. because the broker manages its own
333         // state, plus the only time the _broker will be shutdown or null
334         // is after the last servlet has shutdown--so why would anyone be
335         // accessing us then? if they do the _broker will throw exceptions
336         // complaining that it has been shut down, or they'll get a null here.
337         return _broker;
338     }
339 
340     /***
341      * Get a Log object which can be used to write to the log file.
342      * Messages to the logfile will be associated with the supplied
343      * type. The type name should be short as it may be printed on
344      * every log line. The description is a longer explanation of
345      * the type of messages you intend to write to this Log.
346      */
347     public Log getLog (String type, String description)
348     {
349         return _broker.getLog(type, description);
350     }
351 
352     /***
353      * Get a Log object which can be used to write to the log file.
354      * Messages to the logfile will be associated with the supplied
355      * type. The type will be used as the description.
356      */
357     public Log getLog (String type)
358     {
359         return _broker.getLog(type, type);
360     }
361 
362     /***
363      * Retrieve a template from the "template" provider. Equivalent to
364      * getBroker().get(TemplateProvider.TYPE,key)
365      * @exception NotFoundException if the template was not found
366      * @exception ResourceException if the template coult not be loaded
367      */
368     public Template getTemplate (String key)
369             throws ResourceException
370     {
371         return _wm.getTemplate(key);
372     }
373 
374     /***
375      * Retrieve a URL. This is largely equivalent to creating a URL
376      * object and requesting its content, though it will sit in
377      * WebMacro's cache rather than re-requesting each time.
378      * The content will be returned as an Object.
379      */
380     public String getURL (String url)
381             throws ResourceException
382     {
383         return _wm.getURL(url);
384     }
385 
386 
387     /***
388      * Retrieve configuration information from the "config" provider.
389      * Equivalent to getBroker().get(Config.TYPE,key)
390      * @exception NotFoundException could not locate requested information
391      */
392     public String getConfig (String key)
393             throws NotFoundException
394     {
395         return _wm.getConfig(key);
396     }
397 
398     /***
399      * Retrieve configuration information from the "config" provider.
400      * Return specified default if key could not be found
401      */
402     public String getConfig (String key, String defaultValue)
403     {
404         try
405         {
406             return _wm.getConfig(key);
407         }
408         catch (NotFoundException e)
409         {
410             return defaultValue;
411         }
412     }
413 
414     /***
415      * Create a new Context object
416      */
417     public Context getContext ()
418     {
419         return _wm.getContext();
420     }
421 
422     /***
423      * Create a new WebContext object; can be overridden
424      */
425     public WebContext getWebContext (HttpServletRequest req, HttpServletResponse res)
426     {
427         return _wm.getWebContext(req, res);
428     }
429 
430     /***
431      * Convenience method for writing a template to an OutputStream.
432      * This method takes care of all the typical work involved
433      * in writing a template.<p>
434      *
435      * This method uses the default <code>TemplateOutputEncoding</code> specified in
436      * WebMacro.defaults or your custom WebMacro.properties.
437      *
438      * @param templateName name of Template to write.  Must be accessible
439      *                     via TemplatePath
440      * @param out          where the output of the template should go
441      * @param context      The Context (can be a WebContext too) used
442      *                     during the template evaluation phase
443      * @throws java.io.IOException if the template cannot be written to the
444      *                             specified output stream
445      * @throws ResourceException if the template name specified cannot be found
446      * @throws PropertyException if a fatal error occured during the Template
447      *                           evaluation phase
448      */
449     public void writeTemplate (String templateName, java.io.OutputStream out,
450                                Context context)
451             throws java.io.IOException, ResourceException, PropertyException
452     {
453 
454         writeTemplate(templateName, out,
455                 getConfig(WMConstants.TEMPLATE_OUTPUT_ENCODING),
456                 context);
457     }
458 
459     /***
460      * Convienence method for writing a template to an OutputStream.
461      * This method takes care of all the typical work involved
462      * in writing a template.
463      *
464      * @param templateName name of Template to write.  Must be accessible
465      *                     via TemplatePath
466      * @param out          where the output of the template should go
467      * @param encoding     character encoding to use when writing the template
468      *                     if the encoding is <code>null</code>, the default
469      *                     <code>TemplateOutputEncoding</code> is used
470      * @param context      The Context (can be a WebContext too) used
471      *                     during the template evaluation phase
472      * @throws java.io.IOException if the template cannot be written to the
473      *                             specified output stream
474      * @throws ResourceException if the template name specified cannot be found
475      * @throws PropertyException if a fatal error occured during the Template
476      *                           evaluation phase
477      */
478     public void writeTemplate (String templateName, java.io.OutputStream out,
479                                String encoding, Context context)
480             throws java.io.IOException, ResourceException, PropertyException
481     {
482 
483         if (encoding == null)
484             encoding = getConfig(WMConstants.TEMPLATE_OUTPUT_ENCODING);
485 
486         Template tmpl = getTemplate(templateName);
487         tmpl.write(out, encoding, context);
488     }
489    
490    
491     // DELEGATE-TO METHODS -- COMMON THINGS MADE EASIER
492    
493     /***
494      * This method takes a populated context and a template and
495      * writes out the interpreted template to the context's output
496      * stream.
497      */
498     protected void execute (Template tmpl, WebContext c)
499             throws IOException
500     {
501         try
502         {
503             HttpServletResponse resp = c.getResponse();
504 
505             Locale locale = (Locale) tmpl.getParam(
506                     WMConstants.TEMPLATE_LOCALE);
507             if (_log.loggingDebug())
508                 _log.debug("TemplateLocale=" + locale);
509             if (locale != null)
510             {
511                 setLocale(resp, locale);
512             }
513 
514             String encoding = (String) tmpl.getParam(
515                     WMConstants.TEMPLATE_OUTPUT_ENCODING);
516             if (encoding == null)
517             {
518                 encoding = resp.getCharacterEncoding();
519             }
520 
521             if (_log.loggingDebug())
522                 _log.debug("Using output encoding " + encoding);
523 
524             // get the bytes before calling getOutputStream
525             // this is necessary to be compatible with JSDK 2.3
526             // where you can't call setContentType() after getOutputStream(),
527             // which could be happening during the template evaluation
528             byte[] bytes = tmpl.evaluateAsBytes(encoding, c);
529 
530 
531             // now write the FW buffer to the response output stream
532             writeResponseBytes(resp, bytes, encoding);
533         }
534         catch (UnsupportedEncodingException e)
535         {
536             // can be thrown by FastWriter.getInstance
537             // rethrow it, because otherwise it would be ignored
538             // as an IOException
539             _log.error("tried to use an unsupported encoding", e);
540             throw e;
541         }
542         catch (IOException e)
543         {
544             // ignore disconnect
545         }
546         catch (Exception e)
547         {
548             String error =
549                     "WebMacro encountered an error while executing a template:\n"
550                     + ((tmpl != null) ? (tmpl + ": " + e + "\n") :
551                     ("The template failed to load; double check the "
552                     + "TemplatePath in your webmacro.properties file."));
553             _log.error(error, e);
554             try
555             {
556                 Template errorTemplate = error(c,
557                         "WebMacro encountered an error while executing a template:\n"
558                         + ((tmpl != null) ? (tmpl + ": ")
559                         : ("The template failed to load; double check the "
560                         + "TemplatePath in your webmacro.properties file."))
561                         + "\n<pre>" + e + "</pre>\n");
562 
563                 String err = errorTemplate.evaluateAsString(c);
564                 c.getResponse().getWriter().write(err);
565             }
566             catch (Exception errExcept)
567             {
568                 _log.error("Error writing error template!", errExcept);
569             }
570         }
571     }
572 
573     /***
574      * Helper method to write out a FastWriter (that has bufferd
575      * the response) to a ServletResponse. This method will try to use
576      * the response's OutputStream first and if this fails, fall back
577      * to its Writer.
578      * @param response where to write fast writer to
579      * @param bytes the bytes to write
580      */
581     private void writeResponseBytes (HttpServletResponse response, byte[] bytes, String encoding)
582             throws IOException
583     {
584         OutputStream out;
585         // We'll check, if the OutputStream is available
586         try
587         {
588             out = response.getOutputStream();
589         }
590         catch (IllegalStateException e)
591         {
592             // Here comes a quick hack, we need a cleaner
593             // solution in a future release. (skanthak)
594          
595             // this means, that the ServletOutputStream is
596             // not available, because the Writer has already
597             // be used. We have to use it, although its
598             // much slower, especially, because we need to
599             // revert the encoding process now
600             out = null;
601             _log.debug("Using Writer instead of OutputStream");
602         }
603         response.setContentLength(bytes.length);
604         if (out != null)
605         {
606             out.write(bytes);
607         }
608         else
609         {
610             response.getWriter().write(new String(bytes, encoding));
611         }
612     }
613 
614    
615     // FRAMEWORK TEMPLATE METHODS--PLUG YOUR CODE IN HERE
616    
617    
618     /***
619      * This method is called at the beginning of a request and is
620      * responsible for providing a Context for the request. The
621      * default implementation calls WebContext.newInstance(req,resp)
622      * on the WebContext prototype returned by the initWebContext() method.
623      * This is probably suitable for most servlets, though you can override
624      * it and do something different if you like. You can throw a
625      * HandlerException if something goes wrong.
626      */
627     public WebContext newContext (
628             HttpServletRequest req, HttpServletResponse resp)
629             throws HandlerException
630     {
631         return _wm.getWebContext(req, resp);
632         //return _wcPrototype.newInstance(req, resp);
633     }
634 
635     /***
636      * This method is called to handle the processing of a request. It
637      * should analyze the data in the request, put whatever values are
638      * required into the context, and return the appropriate view.
639      * @return the template to be rendered by the WebMacro engine
640      * @exception HandlerException throw this to produce vanilla error messages
641      * @param context contains all relevant data structures, incl builtins.
642      */
643     public abstract Template handle (WebContext context)
644             throws HandlerException;
645 
646 
647     /***
648      * This method is called at the end of a request and is responsible
649      * for cleaning up the Context at the end of the request. You may
650      * not need to do anything here, but it is sometimes important if
651      * you have an open database connection in your context that you
652      * need to close. The default implementation calls wc.clear().
653      */
654     public void destroyContext (WebContext wc)
655             throws HandlerException
656     {
657     }
658 
659 
660     /***
661      * Override this method to implement any startup/init code
662      * you require. The broker will have been created before this
663      * method is called; the default implementation does nothing.
664      * This is called when the servlet environment initializes
665      * the servlet for use via the init() method.
666      * @exception ServletException to indicate initialization failed
667      */
668     protected void start () throws ServletException
669     {
670     }
671 
672     /***
673      * Override this method to implement any shutdown code you require.
674      * The broker may be destroyed just after this method exits. This
675      * is called when the servlet environment shuts down the servlet
676      * via the shutdown() method. The default implementation does nothing.
677      */
678     protected void stop ()
679     {
680     }
681 
682 
683     /***
684      * This method returns the WebMacro object which will be used to load,
685      * access, and manage the Broker. The default implementation is to
686      * return a new WM() object. You could override it and return a WM
687      * object constructed with a particular configuration file, or some
688      * other implementation of the WebMacro interface.
689      */
690     public WebMacro initWebMacro () throws InitException
691     {
692         return new WM(this);
693     }
694 
695     /***
696      * NO LONGER USED
697      * Exists only to catch implementations that use it.
698      * Use newWebContext instead.
699      * @deprecated
700      */
701     public final WebContext initWebContext () throws InitException
702     {
703         return null;
704     }
705 
706     public WebContext newWebContext(HttpServletRequest req, HttpServletResponse resp) {
707         return new WebContext(_broker, req, resp);
708     }
709 
710     /***
711      * Set the locale on the response.  The reflection trickery is because
712      * this is only defined for JSDK 2.2+
713      */
714     protected void setLocale (HttpServletResponse resp, Locale locale)
715     {
716         try
717         {
718             Method m = HttpServletResponse.class.getMethod(
719                     "setLocale",
720                     new Class[]
721                     {Locale.class});
722             m.invoke(resp, (Object[])new Locale[]
723             {locale});
724             if (_log.loggingDebug())
725                 _log.debug("Successfully set locale to " + locale);
726         }
727         catch (Exception e)
728         {
729             if (_log.loggingDebug())
730                 _log.debug("Error set locale to " + locale + ": " + e.getClass());
731         }
732     }
733 
734     /***
735      * Get a new FastWriter.
736      * A FastWriter is used when writing templates to an output stream
737      *
738      * @param out The output stream the FastWriter should write to.  Typically
739      *           this will be your ServletOutputStream
740      * @param enctype the Encoding type to use
741      * @deprecated
742      */
743     public FastWriter getFastWriter (OutputStream out, String enctype)
744             throws UnsupportedEncodingException
745     {
746         return _wm.getFastWriter(out, enctype);
747     }
748 
749 
750     private static final String DEFAULT_ERROR_TEXT =
751             "<HTML><HEAD><TITLE>Error</TITLE></HEAD>\n"
752             + "#set $Response.ContentType = \"text/html\"\n"
753             + "<BODY><H1>Error</H1>"
754             + "<HR>$error</BODY></HTML>";
755 
756     private Template _errorTemplate = null;
757 
758     /***
759      * Gets a template for displaying an error message.  
760      * Tries to get configured template, then default template
761      * and lastly constructs a string template.
762      * @return A Template which can be used to format an error message
763      */
764     public Template getErrorTemplate ()
765     {
766         String templateName = getErrorTemplateName();
767 
768         try
769         {
770             _errorTemplate = (Template) _broker.get("template", templateName);
771         }
772         catch (ResourceException e)
773         {
774             _errorTemplate = new org.webmacro.engine.StringTemplate(_broker, DEFAULT_ERROR_TEXT,
775                     "WebMacro default error template");
776         }
777         return _errorTemplate;
778     }
779 
780     protected String getErrorTemplateName ()
781     {
782         String templateName;
783 
784         try
785         {
786             templateName = (String) _broker.get("config", ERROR_TEMPLATE);
787         }
788         catch (ResourceException e)
789         {
790             templateName = ERROR_TEMPLATE_DEFAULT;
791         }
792         return templateName;
793     }
794 
795 
796 }